﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Controls.Interfaces;
using Silvermoon.OpenGL;
using System.Drawing;
using Silvermoon.Controls.Panoramic;
using Silvermoon.UI;
using Silvermoon.Shapes;
using Silvermoon.Animations;
using System.Diagnostics;

namespace Silvermoon.Controls
{
    /// <summary>
    /// Panorama View.
    /// </summary>
    public class Panorama : CompositeControlBase, INotifyPanoramaSectionChanged
    {
        #region const

        const int marginLeft = 24;
        const int marginWidth = 24;
        const int titleHeight = 120;
        const float panoramaImageFactor = 0.20f;
        const float titleFactor = 0.30f;

        #endregion
        #region static

        public static FontSprite DefaultTitleFont { get; private set; }

        #endregion
        #region accessors

        public static readonly PropertyAccessor<Panorama, float> JumpOffsetProperty = PropertyAccessor.Register<Panorama, float>("JumpOffset",
            c => c.jumpOffset,
            (c, v) => c.JumpOffset = v);


        public static readonly PropertyAccessor<Panorama, int> OffsetProperty = PropertyAccessor.Register<Panorama, int>("Offset",
            c => c.xOffset,
            (c, v) => c.Offset = v);

        public static readonly PropertyAccessor<Panorama, int> PanoramaImageOpacityProperty = PropertyAccessor.Register<Panorama, int>("PanoramaImageOpacity",
            c => c.PanoramaImageOpacity,
            (c, v) => c.PanoramaImageOpacity = v);

        #endregion
        #region fields

        private int xOffset = 0;
        private float jumpOffset = 0;
        private StringShape titleShape;
        private ImageShape imageShape;
        private StringShape jumpTitleShape;
        private ImageShape jumpImageShape;

        #endregion
        #region ctor/dtor

        /// <summary>
        /// Initializes the <see cref="Hub"/> class.
        /// </summary>
        static Panorama()
        {
            DefaultTitleFont = FontSprite.FromData(GlFontFamily.GenericSansSerif, 24, FontStyle.Regular, false);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Hub"/> class.
        /// </summary>
        public Panorama()
            : base()
        {
            ControlFlags |= ControlFlags.CanPan;
            Sections = new PanoramaSectionCollection(this);
            titleShape = new StringShape { Font = DefaultTitleFont, Alignment = Alignment.Near, LineAlignment = Alignment.Center, FontScale = 1.4f, AutoSize = true };
            titleShape.Transformations = new Transformation[] { new TranslateTransform(0f, 0f, 50f) };
            jumpTitleShape = new StringShape { Font = DefaultTitleFont, Alignment = Alignment.Near, LineAlignment = Alignment.Center, FontScale = 1.4f, AutoSize = true };
            jumpTitleShape.Transformations = new Transformation[] { new TranslateTransform(0f, 0f, 50f) };

            imageShape = new ImageShape { Color = Color.White };
            imageShape.Transformations = new Transformation[] { new TranslateTransform(0f, 0f, 80f) };
            jumpImageShape = new ImageShape { Color = Color.White };
            jumpImageShape.Transformations = new Transformation[] { new TranslateTransform(0f, 0f, 80f) };

            jumpImageShape.Visible = false;
            jumpTitleShape.Visible = false;

            Shapes.AddLast(jumpImageShape);
            Shapes.AddLast(imageShape);
            Shapes.AddLast(jumpTitleShape);
            Shapes.AddLast(titleShape);
            Margin = Margin.Zero;
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool dispose)
        {
            ClearSections();
            base.Dispose(dispose);
        }

        #endregion
        #region props

        public FontSprite TitleFont
        {
            get { return titleShape.Font; }
            set
            {
                if (titleShape.Font != value)
                {
                    titleShape.Font = value;
                    jumpTitleShape.Font = value;
                    
                    Invalidate(InvalidationFlag.LayoutAppearance);
                }
            }
        }

        public float TitleFontScale
        {
            get { return titleShape.FontScale; }
            set
            {
                if (titleShape.FontScale != value)
                {
                    titleShape.FontScale = value;
                    jumpTitleShape.FontScale = value;
                    Invalidate(InvalidationFlag.Appearance);
                }
            }
        }

        public Color TitleColor
        {
            get { return titleShape.Color; }
            set
            {
                if (titleShape.Color != value)
                {
                    titleShape.Color = value;
                    jumpTitleShape.Color = value;
                    Invalidate(InvalidationFlag.Color);
                }
            }
        }

        public int PanoramaImageOpacity
        {
            get { return imageShape.Opacity; }
            set
            {
                if (imageShape.Opacity != value)
                {
                    imageShape.Opacity = value;
                    jumpImageShape.Opacity = value;
                    Invalidate(InvalidationFlag.Appearance);
                }
            }
        }

        public int CurrentSectionIndex
        {
            get
            {
                int x = -xOffset;
                if (x < 0) x += TotalWidth;
                else if (x >= MaxXOffset) x -= TotalWidth;

                int w2 = Width / 2;
                int index = 0;
                foreach (PanoramaSection s in Sections)
                {
                    int left = s.Left + xOffset;
                    if ((left - w2) <= 0 && (left + w2) >= 0)
                    {
                        return index;
                    }
                    index++;
                }
                return -1;
            }
        }

        public int TotalWidth
        {
            get
            {
                int width = 0;
                foreach (PanoramaSection section in Sections) width += section.Width + marginWidth;
                return width;
            }
        }

        public int MaxXOffset
        {
            get
            {
                int width = 0;
                for (int i = 0; i < Sections.Count - 1; i++)
                {
                    width += Sections[i].Width + marginWidth;
                }
                return width;
            }
        }

        public int Offset
        {
            get { return xOffset; }
            set
            {
                if (xOffset != value)
                {
                    int old = xOffset;
                    xOffset = value;
                    OnXOffsetChanged(old);
                }
            }
        }

        /// <summary>
        /// This property is jused to perform animation when to jump from the first section to the last and vice versa.
        /// </summary>
        protected float JumpOffset
        {
            get { return jumpOffset; }
            set
            {
                if (jumpOffset != value)
                {
                    float old = jumpOffset;
                    jumpOffset = value;
                    OnJumpOffsetChanged(old);
                }
            }
        }

        /// <summary>
        /// Gets the collection of sections.
        /// </summary>
        public PanoramaSectionCollection Sections { get; private set; }

        /// <summary>
        /// Gets or sets the panorama title.
        /// </summary>
        public string Title
        {
            get { string text = titleShape.Text; return text ?? string.Empty; }
            set
            {
                if (titleShape.Text != value)
                {
                    titleShape.Text = value;
                    jumpTitleShape.Text = value;
                    OnTitleChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the image for the panorama background.
        /// </summary>
        public Texture PanoramaImage
        {
            get { return imageShape.Image; }
            set
            {
                if (imageShape.Image != value)
                {
                    imageShape.Image = value;
                    jumpImageShape.Image = value;
                    OnPanoramaImageChanged();
                }
            }
        }

        #endregion
        #region methods

        protected virtual void OnJumpOffsetChanged(float oldValue)
        {
            Invalidate(InvalidationFlag.Layout);
        }

        protected virtual void OnXOffsetChanged(int oldValue)
        {
            Invalidate(InvalidationFlag.Layout);
        }

        protected virtual void OnTitleChanged()
        {
            Invalidate(InvalidationFlag.Appearance);
        }

        protected virtual void OnSectionsChanged()
        {
            Invalidate(InvalidationFlag.LayoutAppearance | InvalidationFlag.Internal);
        }

        protected virtual void OnPanoramaImageChanged()
        {
            Invalidate(InvalidationFlag.Appearance | InvalidationFlag.Layout);
        }

        protected override void OnLayout()
        {
            base.OnLayout();
            if (CheckFlag(InvalidationFlag.Width | InvalidationFlag.Size | InvalidationFlag.Layout | InvalidationFlag.Internal))
            {
                LayoutSections();
            }
            if (CheckFlag(InvalidationFlag.Width | InvalidationFlag.Size | InvalidationFlag.LayoutAppearance))
            {
                LayoutPanorama();
                DisableUnusedSections();
            }
        }

        private void DisableUnusedSections()
        {
            Rectangle client = ClientBounds;
            foreach (PanoramaSection section in Sections)
            {
                section.Visible = client.IntersectsWith(section.Bounds);
            }
        }

        private void LayoutPanorama()
        {
            Texture image = PanoramaImage;

            bool isJumpVisible = jumpOffset != 0;
            jumpTitleShape.Visible = isJumpVisible;

            imageShape.Visible = image != null && image.Height > 0;
            if (imageShape.Visible)
            {
                Size imageSize = image.Size;
                int offset = xOffset - Width - (int)(jumpOffset * TotalWidth);
                int jump = jumpOffset == 0f ? 0 : (int)(jumpOffset * imageShape.Width);
                int imageLeft = (int)(panoramaImageFactor * offset) + jump;
                int imageWidth = imageSize.Width * Height / imageSize.Height;
                imageShape.Bounds = Control.ScaledRect(imageLeft, 0, imageWidth, Height);
                if (imageShape.Bounds.Right < Width)
                {
                    imageLeft += imageShape.Width;
                    jumpImageShape.Bounds = Control.ScaledRect(imageLeft, 0, imageWidth, Height);
                    jumpImageShape.Visible = true;
                }
                else if (imageShape.X > 0)
                {
                    imageLeft -= imageShape.Width;
                    jumpImageShape.Bounds = Control.ScaledRect(imageLeft, 0, imageWidth, Height);
                    jumpImageShape.Visible = true;
                }
                else
                {
                    jumpImageShape.Visible = false;
                }
            }
            titleShape.Visible = !string.IsNullOrEmpty(Title);
            if (titleShape.Visible)
            {
                //int jumpTitleWidth = Math.Max(titleShape.Width, TotalWidth);
                int jumpTitleWidth = !isJumpVisible ? 0 : Math.Max(titleShape.Width + Width / 2, (int)((float)Width / titleFactor));
                int offset = xOffset - 0 - (int)(jumpOffset * TotalWidth);
                int jump = !isJumpVisible ? 0 : (int)(jumpTitleWidth * jumpOffset);
                int titleLeft = (int)(titleFactor * offset) + marginLeft + jump;
                titleShape.Location = Control.ScaledPoint(titleLeft + 8, 8);
                titleShape.Size = titleShape.Size;
                if (isJumpVisible)
                {
                    if (jumpOffset > 0f)
                    {
                        titleLeft -= jumpTitleWidth;
                    }
                    else
                    {
                        titleLeft += jumpTitleWidth;
                    }
                    jumpTitleShape.Location = Control.ScaledPoint(titleLeft + 8, 8);
                    jumpTitleShape.Size = jumpTitleShape.Size;
                }
            }
        }

        private void LayoutSections()
        {
            int h = Math.Max(0, Height - titleHeight);
            int top = titleHeight;
            int left = xOffset + marginLeft;
            Size size = this.Size;

            // adding marginLeft to peek the next section:
            size.Width -= marginWidth + 2 * marginLeft;

            size.Height -= titleHeight;
            foreach (PanoramaSection section in Sections)
            {
                Rectangle bounds = section.Bounds;
                bounds.Location = Point.Empty;
                bounds.Height = h;
                bounds = section.Margin.Layout(bounds, size);
                bounds.Y += top;
                bounds.X += left;
                section.Bounds = bounds;
                left += bounds.Width + marginWidth;
            }
            if (Sections.Count > 1)
            {
                if (xOffset > 0)
                {
                    PanoramaSection section = Sections[Sections.Count - 1];
                    Rectangle bounds = section.Bounds;
                    bounds.Location = Point.Empty;
                    bounds = section.Margin.Layout(bounds, size);
                    bounds.X = xOffset - bounds.Width - marginWidth;
                    bounds.Y += top;
                    section.Bounds = bounds;
                }
                else if (-xOffset > (MaxXOffset))
                {
                    PanoramaSection section = Sections[0];
                    Rectangle bounds = section.Bounds;
                    bounds.Location = Point.Empty;
                    bounds = section.Margin.Layout(bounds, size);
                    bounds.X += left + marginWidth;
                    bounds.Y += top;
                    section.Bounds = bounds;
                }
            }
        }

        protected virtual void OnSectionSizeChanged(object sender, EventArgs e)
        {
            Invalidate(InvalidationFlag.Layout);
        }

        protected internal override void OnBeginPan(PanEventArgs e)
        {
            OffsetProperty.StopAnimation(this);
            if (!e.Handled && e.HorizontalMotion)
            {
                e.Handled = true;
                e.Value = new Point(xOffset, 0);
            }
            base.OnBeginPan(e);
        }

        protected internal override void OnEndPan(EventArgs e)
        {
            if (!OffsetProperty.IsAnimated(this)) EnsureRestState(true);
            base.OnEndPan(e);
        }

        protected internal override void OnPan(PanEventArgs e)
        {
            if (!e.Handled)
            {
                Offset = e.Value.X;
                e.Handled = true;
            }
            base.OnPan(e);
        }

        protected override void EnsureRestStateOnSizeChanged()
        {
            //base.EnsureRestStateOnSizeChanged();
        }

        /// <summary>
        /// Sets all properties to a defined value.
        /// If overridden, this method determines the ideal values of specific properties, for instance, when a property is changed as the
        /// uses swipes between possible values.
        /// It's best practice to access each property value  using PropertyAccessor.GetUnanimatedValue.
        /// </summary>
        /// <param name="animated">True when to allowed to tranistion to a new value, otherwise false.</param>
        public override void EnsureRestState(bool animated)
        {
            int xOffset = -OffsetProperty.GetUnanimatedValue(this);

            if (xOffset < 0)
            {
                PanoramaSection s = Sections[Sections.Count - 1];
                if ((-xOffset) >= s.Width / 2) FlickToPreviousSection(); else FlickToNextSection();
            }
            else if (xOffset > MaxXOffset)
            {
                int w = xOffset - MaxXOffset;
                PanoramaSection s = Sections[0];
                if (w <= s.Width / 2) FlickToPreviousSection(); else FlickToNextSection();
            }
            else
            {
                int w = Width;
                int w2 = w / 2;
                int minX = xOffset - w2;
                int maxX = xOffset + w2;
                int x = 0;

                foreach (PanoramaSection section in Sections)
                {
                    int sw = section.Width;
                    if (sw <= Width)
                    {
                        // if the width of the section is smaller or equal than the width, xoffset is the left of the section if it is close before or after xOffset:
                        if (minX <= x && maxX >= x)
                        {
                            if (xOffset != x) ScrollTo(-x, true);
                            break;
                        }
                    }
                    else
                    {
                        // if the width of the section is larger than the width, xOffset is either the left of the section close after xOffset (but not close before):
                        if (xOffset < x && maxX > x)
                        {
                            if (xOffset != x) ScrollTo(-x, true);
                            break;
                        }
                        else
                        {
                            // ... or the right is is close after the right edge (but not close before):
                            if (maxX >= x)
                            {
                                int x1 = x + sw + marginWidth - Width;
                                if (x1 > (xOffset - w2) && x1 < xOffset)
                                {
                                    if (xOffset != x) ScrollTo(-x1, true);
                                    break;
                                }
                            }
                        }
                    }
                    x += sw + marginWidth;
                }
            }
        }

        protected internal override void OnFlick(TouchEventArgs e)
        {
            if (e.HorizontalMotion)
            {
                e.Handled = true;
                if (e.Velocity >= TouchEventArgs.LongFlickVelocity)
                {
                    if (e.Direction == PanDirection.Left)
                    {
                        FlickToNextSection();
                    }
                    else
                    {
                        FlickToPreviousSection();
                    }
                }
            }
            base.OnFlick(e);
        }

        private void FlickToPreviousSection()
        {
            int x = -xOffset;
            if (x >= 0)
            {
                int offset = TotalWidth;
                for (int i = Sections.Count - 1; i >= 0; i--)
                {
                    PanoramaSection section = Sections[i];
                    if (offset < x) break;
                    offset -= section.Width + marginWidth;
                }
                ScrollTo(-offset, true);
            }
            else
            {
                Offset -= TotalWidth;
                AnimateJump(-1f);
                ScrollTo(-MaxXOffset, true);
            }
        }

        private void FlickToNextSection()
        {
            int x = -xOffset;

            int offset = 0;
            foreach (PanoramaSection s in Sections)
            {
                if (offset >= x) break;
                offset += s.Width + marginWidth;
            }
            if (offset <= MaxXOffset)
            {
                ScrollTo(-offset, true);
            }
            else
            {
                PanoramaSection section = Sections[Sections.Count - 1];
                Offset += TotalWidth;
                AnimateJump(1f);
                ScrollTo(0, true);
            }
        }

        private void AnimateJump(float startValue)
        {
            FloatTransition transition = new FloatTransition
            {
                Duration = 550,
                From = startValue,
                To = 0f,
                EasingFunction = PowerEase.Out,
                Tracking = TrackMode.Keep
            };
            JumpOffsetProperty.BeginAnimation(this, transition);
        }

        private void ScrollTo(int offset, bool animated)
        {
            if (xOffset != offset)
            {
                IntTransition transition = new IntTransition
                {
                    Property = OffsetProperty,
                    Duration = 350,
                    To = offset,
                    EasingFunction = ExponentialEase.In,
                    Tracking = TrackMode.Keep
                };
                OffsetProperty.BeginAnimation(this, transition);
            }
            else
            {
                OffsetProperty.StopAnimation(this);
                Offset = offset;
            }
        }

        #endregion
        #region INotifyHubSectionChanged Members

        void INotifyPanoramaSectionChanged.SectionAdded(PanoramaSection c)
        {
            OnSectionsChanged();
            c.SizeChanged += new EventHandler(OnSectionSizeChanged);
            VisibleControls.Add(c);
        }

        void INotifyPanoramaSectionChanged.CollectionCleared()
        {
            foreach (PanoramaSection c in Sections) VisibleControls.Remove(c);
            OnSectionsChanged();
            ClearSections();
        }

        private void ClearSections()
        {
            foreach (PanoramaSection c in Sections) c.SizeChanged -= OnSectionSizeChanged;
        }

        void INotifyPanoramaSectionChanged.SectionRemoved(PanoramaSection c)
        {
            OnSectionsChanged();
            c.SizeChanged -= OnSectionSizeChanged;
            VisibleControls.Remove(c);
        }

        #endregion
    }
}
